Super Easy Formで超簡単にサーバーレスなHTMLフォームを作ってみる
CX事業本部の阿部です。
同僚から教えてもらったサーバーレスなバックエンドをもつフォーム作成用のツールを試してみたので、その内容をまとめます。
Super Easy Form is 何
サーバーレスなバックエンドを持ったレスポンシブなHTMLフォームを簡単に作るためのツールです。
特徴
公式のランディングページより。
- CLIでプロジェクト作成からデプロイまで可能
- HTMLのカスタマイズが可能(iframe未使用)
- e-mailでの通知が可能
- サブミットしたフォームのデータをCSVやJSONにエクスポート可能
- オプショナルでCAPTCHAが使える
システム要件
- Node.jsのバージョンが10.x以上
- AWSのアカウントを持っていること(デプロイに必要)
使ってみる
コマンドラインツールのインストール
npmでインストールが可能です。ランディングページに記載の以下のコマンドでグローバルにインストールしましょう。
npm i -g super-easy-forms-cli
インストール後のコマンドは sef
です。インストールされたか確認してみます。
$ npm i -g super-easy-forms-cli /usr/local/bin/sef -> /usr/local/lib/node_modules/super-easy-forms-cli/bin/run + [email protected] added 79 packages from 38 contributors in 6.388s $ sef --version super-easy-forms-cli/1.1.0 darwin-x64 node-v12.12.0
ちゃんとインストールされたようです。
プロジェクトを作成する
Getting Startedの流れでプロジェクトを作成してみます。
今回は sef-test
というプロジェクト名でフォームを作成してデプロイまでしてみましょう。
オレゴンリージョン us-west-1
に私の開発用のプロファイル personal_dev
を使ってデプロイするようにプロジェクトを作成します。
なお、オレゴンリージョンを選んだ理由は、フォーム作成時に通知用のemail送信のためSESを利用していて、このリージョンがSESに対応している必要があるからです。
実行前に build
サブコマンドのヘルプを確認してみます。
$ sef build --help Builds the required base files and directories. USAGE $ sef build OPTIONS -p, --profile=profile The name of the iam profile/user that you want to create -r, --region=region The desired AWS region were your forms infrastructure will be deployed
これを見ると profile
オプションはこのフォーム作成に合わせて新しくプロファイルを作る場合に必要なようです。
今回は私の検証環境のプロファイルをそのまま使う予定なので、このオプションは外て実行します。
$ sef build -r=us-west-2 Created the ./forms directory Created the ./settings.json file
$ ls -lart total 24 drwxr-xr-x 6 abe.shinsuke staff 192 4 9 09:28 .. drwxr-xr-x 2 abe.shinsuke staff 64 4 9 09:28 forms -rw-r--r-- 1 abe.shinsuke staff 2 4 9 09:28 settings.json -rw-r--r-- 1 abe.shinsuke staff 46 4 9 09:28 .env drwxr-xr-x 6 abe.shinsuke staff 192 4 9 09:28 . -rw-r--r-- 1 abe.shinsuke staff 4 4 9 09:28 .gitignore
各種ファイルが生成されたようです。
forms
というサブディレクトリがあるので、一つのプロジェクトで複数のフォームが管理できるようですね。
なお、この段階では、 forms
は空ディレクトリです。
また、seggings.jsonにも何も記載がありません。
$ cat settings.json {}
フォームを作ってみる
Getting Startedの内容でフォームを作ってみます。以下のコマンドを流します。 なお、このコマンドを流す際には、SESはサンドボックスから出る必要があります。
$ sef fullform myform --email=私のメールアドレス --fields=fullName=text=required,email=email=required,message=text=required
なお、この時に試してみましたが、 フォーム名は英数字以外NG でした。
$ sef fullform myform --email=私のメールアドレス --fields=fullName=text=required,email=email=required,message=text=required Error: ENOENT: no such file or directory, open './forms/myform/config.json' at FullformCommand.run (/usr/local/lib/node_modules/super-easy-forms-cli/src/commands/fullform.js:98:24) at FullformCommand._run (/usr/local/lib/node_modules/super-easy-forms-cli/node_modules/@oclif/command/lib/command.js:42:31)
おや?
./forms/myform/config.json
がないと言われますね。
確かにないです。
ちょっと fullform
のヘルプを参照してみましょう。
$ sef fullform --help Generates an html form and saves it in the formNames folder USAGE $ sef fullform NAME ARGUMENTS NAME name of the form - must be unique OPTIONS -c, --captcha Adds recaptcha elements and scripts to the form and lambda function -e, --email=email Email address that will be used to send emails -f, --fields=fields Desired form formFields -l, --labels Automatically add labels to your form -m, --message=message the email message body. you can use html and you can use <FormOutput> to include the information from the form submission -r, --recipients=recipients Recipients that will recieve emails on your behalf. -s, --subject=subject the subject of the email message
Generates an html form and saves it in the formNames folder
とあるので、フォーム名のディレクトリも一緒に作成してくれそうなものですがそうではないのでしょうか?
他にその前に使えそうなサブコマンドがないかみてみます。
$ sef --help a CLI for super-easy-forms VERSION super-easy-forms-cli/1.1.3 darwin-x64 node-v12.12.0 USAGE $ sef [COMMAND] COMMANDS build Builds the required base files and directories. delete Deletes all resources in the AWS cloud for the desired form deploy Deploys your stack in the AWS Cloud email Verifies/validates your email with AWS SES form Builds an html form fullform Generates an html form and saves it in the formNames folder help display help for sef iam the --create flag will open up a window with the AWS console so that you confirm the creation of a user with the entered name. init Creates a config file with empty values for your form. lambda Creates or updates a lambda function and optionally zips and uploads it into an AWS s3 bucket. submissions export or list all of the suibmissions you have had to date for a selected form template validate/create/update your cloudformation template saved locally variable Builds an html form
init
が使えそうですね。
というわけで sef init
を実行してから再度 sef fullform
を実行します。
これがバグなのかGetting Startedの記載不備なのかは、現時点では判断つかず。
bash-3.2$ sef fullform myform --email=私のメールアドレス --fields=fullName=text=required,email=email=required,message=text=required Setting up... done Verifying email... done Generating your lambda function... done Cloudformation template for form myform has been saved Generating your cloudformation template... done Creating your stack in the AWS cloud... done Fetching your API enpoint URL... done Generating your form... done
なお、メールアドレスのverifyが飛んできますが、ここは時間がかかるのか少し待たされました。
フォームのコード生成と同時にCloudFormationのスタックを作ってデプロイもしてくれるようで、全て終了したらフォームのURL(ローカルにあるHTMLファイル)もブラウザで開いてくれました。
フォームにデータを送信してみる
入力してフォームを送信してみます。
DynamoDBを確認。データは登録されているようですね。
フォームの構成をみてみる
$ ls -lart total 440 drwxr-xr-x 3 abe.shinsuke staff 96 4 23 15:36 .. drwxr-xr-x 4 abe.shinsuke staff 128 4 23 15:45 myformFunction -rw-r--r-- 1 abe.shinsuke staff 207088 4 23 15:45 myformFunction.zip -rw-r--r-- 1 abe.shinsuke staff 5384 4 23 15:45 template.json -rw-r--r-- 1 abe.shinsuke staff 560 4 23 15:46 config.json drwxr-xr-x 7 abe.shinsuke staff 224 4 23 15:46 . -rw-r--r--@ 1 abe.shinsuke staff 3474 4 23 15:46 myform.html
myformsFunction
の配下をみてみましょう。
$ ls -lart total 8 drwxr-xr-x 7 abe.shinsuke staff 224 4 23 15:45 node_modules drwxr-xr-x 4 abe.shinsuke staff 128 4 23 15:45 . -rw-r--r-- 1 abe.shinsuke staff 1634 4 23 15:45 index.js drwxr-xr-x 7 abe.shinsuke staff 224 4 23 15:46 ..
index.js
の中身はこのようになっています。
const uuidv1 = require('uuid/v1'); const axios = require('axios').default; var AWS = require('aws-sdk'); var ses = new AWS.SES({apiVersion: '2010-12-01'}); var dynamodb = new AWS.DynamoDB({apiVersion: '2012-08-10'}); exports.handler = (event, context, callback) => { let obj = {"id":"id","fullName":"fullName","email":"email","message":"message"}; let uid = uuidv1(); let dbobj = {}; Object.keys(obj).map(function(key, index) { obj[key] = event[key]; dbobj[key] = {S:event[key]}; }) obj['id'] = uid; dbobj['id'] = {S:uid}; let params = { Item: dbobj, TableName: "myform", }; var formOutput = '<br>'; for(let item in obj){ formOutput += '<span><b>' + item + ': </b>' + obj[item] + '</span><br>'; } var emailBody = ''.replace('<FormOutput>', formOutput) dynamodb.putItem(params, function(err, data) { if (err) { callback(err); } else { var params = { Destination: { ToAddresses: ["私のメールアドレス"] }, Message: { Body: { Html: { Charset: "UTF-8", Data: emailBody }, Text: { Charset: "UTF-8", Data: "empty" } }, Subject: { Charset: "UTF-8", Data: "" } }, ReplyToAddresses: [], Source: "私のメールアドレス", }; ses.sendEmail(params, function(err, data) { if (err) { callback(err); } else { callback(null, 'Success'); } }); } }); };
イベントからフォームを取得して、DynamoDBにput、その後SESで指定したメールアドレスに送る、という流れのシンプルなLambda Functionです。
そのほかのファイルも確認します。
config.json
を見てみる
整形して表示します。
{ "email": "私のメールアドレス", "emailSubject": "", "emailMessage": "", "recipients": [], "formFields": { "fullName": { "type": "text", "label": "Full Name", "required": true }, "email": { "type": "email", "label": "Email", "required": true }, "message": { "type": "text", "label": "Message", "required": true } }, "captcha": false, "zip": true, "functionBucket": true, "endpointUrl": "APIのエンドポイント", "stackId": "CloudFormationのStackID", "restApiId": "REST APIのID" }
フォーム作成時に設定した内容と作成したフォームのAPIのエンドポイントなどの情報が書かれています。
template.json
を見てみる
これも長いので整形しました。 作成されるリソースはAPI Gateway/Lamdba/DynamoDBのテーブルとその関連リソース、およびIAMですね。
フォームの静的リソースはこのコマンドではデプロイしないようです。
{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RestApi": { "Type": "AWS::ApiGateway::RestApi", "Properties": { "Name": "myformRestApi", "Description": "The REST API for for your Super Easy Form", "EndpointConfiguration": { "Types": [ "REGIONAL" ] } } }, "ApiModel": { "Type": "AWS::ApiGateway::Model", "Properties": { "ContentType": "application/json", "Name": "myformApiModel", "RestApiId": { "Ref": "RestApi" }, "Schema": { "$schema": "http://json-schema.org/draft-04/schema#", "title": "myform", "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string" }, "fullName": { "type": "string" }, "email": { "type": "string" }, "message": { "type": "string" } }, "required": [ "id", "fullName", "email", "message" ] } } }, "ApiValidator": { "Type": "AWS::ApiGateway::RequestValidator", "Properties": { "RestApiId": { "Ref": "RestApi" }, "Name": "myformValidation", "ValidateRequestBody": true, "ValidateRequestParameters": true } }, "ApiPostMethod": { "Type": "AWS::ApiGateway::Method", "Properties": { "AuthorizationType": "NONE", "HttpMethod": "POST", "ResourceId": { "Fn::GetAtt": [ "RestApi", "RootResourceId" ] }, "RestApiId": { "Ref": "RestApi" }, "ApiKeyRequired": false, "Integration": { "Type": "AWS", "IntegrationHttpMethod": "POST", "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "LambdaFunction", "Arn" ] }, "/invocations" ] ] }, "IntegrationResponses": [ { "ResponseTemplates": { "application/json": "$input.json('$.body')" }, "ResponseParameters": { "method.response.header.Link": "integration.response.body.headers.next", "method.response.header.Access-Control-Allow-Origin": "'*'" }, "StatusCode": 200 } ] }, "RequestValidatorId": { "Ref": "ApiValidator" }, "MethodResponses": [ { "ResponseModels": { "application/json": { "Ref": "ApiModel" } }, "ResponseParameters": { "method.response.header.Link": true, "method.response.header.Access-Control-Allow-Origin": false }, "StatusCode": 200 } ] }, "DependsOn": [ "LambdaFunction" ] }, "ApiOptionsMethod": { "Type": "AWS::ApiGateway::Method", "Properties": { "AuthorizationType": "NONE", "HttpMethod": "OPTIONS", "ResourceId": { "Fn::GetAtt": [ "RestApi", "RootResourceId" ] }, "RestApiId": { "Ref": "RestApi" }, "ApiKeyRequired": false, "Integration": { "IntegrationHttpMethod": "OPTIONS", "Type": "MOCK", "RequestTemplates": { "application/json": "{\n \"statusCode\": 200\n}" }, "PassthroughBehavior": "WHEN_NO_MATCH", "TimeoutInMillis": 29000, "CacheNamespace": { "Fn::GetAtt": [ "RestApi", "RootResourceId" ] }, "Uri": { "Fn::Join": [ "", [ "arn:aws:apigateway:", { "Ref": "AWS::Region" }, ":lambda:path/2015-03-31/functions/", { "Fn::GetAtt": [ "LambdaFunction", "Arn" ] }, "/invocations" ] ] }, "IntegrationResponses": [ { "ResponseTemplates": { "application/json": "" }, "ResponseParameters": { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", "method.response.header.Access-Control-Allow-Origin": "'*'" }, "StatusCode": 200 } ] }, "MethodResponses": [ { "ResponseModels": { "application/json": "Empty" }, "ResponseParameters": { "method.response.header.Access-Control-Allow-Headers": false, "method.response.header.Access-Control-Allow-Methods": false, "method.response.header.Access-Control-Allow-Origin": false }, "StatusCode": 200 } ] }, "DependsOn": [ "LambdaFunction" ] }, "DynamoDbTable": { "Type": "AWS::DynamoDB::Table", "Properties": { "AttributeDefinitions": [ { "AttributeName": "id", "AttributeType": "S" } ], "KeySchema": [ { "AttributeName": "id", "KeyType": "HASH" } ], "TableName": "myform", "BillingMode": "PAY_PER_REQUEST" } }, "LambdaFunction": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": "myformfunction", "S3Key": "myformFunction.zip" }, "Description": "This Lambda Function Adds your contact info. to a Dynamo DB table and then sends you an email.", "Environment": { "Variables": { "RECAPTCHA_SECRET": "undefined" } }, "FunctionName": "myformFunction", "Handler": "index.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "IamRole", "Arn" ] }, "Runtime": "nodejs10.x", "Tags": [ { "Key": "formName", "Value": "myform" } ], "Timeout": 30 }, "DependsOn": [ "DynamoDbTable", "IamRole" ] }, "LambdaPermission": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Ref": "LambdaFunction" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { "Fn::Join": [ "", [ "arn:aws:execute-api:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":", { "Ref": "RestApi" }, "/*/POST/" ] ] } }, "DependsOn": [ "ApiPostMethod" ] }, "IamPolicy": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem" ], "Resource": { "Fn::GetAtt": [ "DynamoDbTable", "Arn" ] } }, { "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:aws:ses:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":identity/[email protected]" ] ] }, "Action": [ "SES:SendEmail", "SES:SendRawEmail" ] } ] }, "PolicyName": "myformPolicy", "Roles": [ { "Ref": "IamRole" } ] } }, "IamRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Description": "Role that allows the Lambda function to interact with the IAM policy", "RoleName": "myformFormRole" } }, "ApiDeployment": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "Description": "deployment of the REST API for the myform form", "RestApiId": { "Ref": "RestApi" }, "StageName": "DeploymentStage" }, "DependsOn": [ "ApiPostMethod" ] } } }
その他のファイルについて
myform.html
と myformFunction.zip
があります。
myform.html
はフォームのHTMLファイルです。 Super Easy FormsのCustomize Your Formsのセクション にある通り、フォームのデザインなどを変更したいときは、このファイルを直接編集すればいいようです。
実際の運用では、S3に置いてCloudFrontでフォームを提供する、という形になるのではないかと思います。
REST APIは作られているので、上記構成に頼らなくてもAPIにアクセスできるのであれば、どこでもいいでしょうが。
myformFunction.zip
はCloudFormationが使うLambda Functionのコードのリソースです。
まとめ
名前の通り非常に簡単に作成することができました。
フォームのカスタマイズなどはしていませんが、デザインのカスタマイズであればバックエンドを意識することなくできそうです。